# Copyright 2004-2005 Daniel Henninger <jadestorm@nc.rr.com>
# Licensed for distribution under the GPL version 2, check COPYING for details

import utils
if utils.checkTwisted():
	from twisted.xish.domish import Element
	from twisted.words.protocols.jabber import jid
else:
	from tlib.domish import Element
	from tlib.jabber import jid
from twisted.internet.defer import Deferred
from twisted.internet import reactor
import sys
import config
import legacy
import debug
import lang
import globals


class ServerDiscovery:
	""" Handles everything IQ related. You can send IQ stanzas and receive a Deferred
	to notify you when a response comes, or if there's a timeout.
	Also manages discovery for server & client """
 	 
	# TODO rename this file & class to something more sensible

	def __init__ (self, pytrans):
		debug.log("ServerDiscovery: Created server discovery manager")
		self.pytrans = pytrans
		self.identities = {}
		self.features = {}
		self.nodes = {}
		self.deferredIqs = {} # A dict indexed by (jid, id) of deferreds to fire
		
		self.addFeature(globals.DISCO, None, config.jid)
		self.addFeature(globals.DISCO, None, config.confjid)
		self.addFeature(globals.DISCO, None, "USER")

	def sendIq(self, el, timeout=15):
		""" Used for sending IQ packets.
		The id attribute for the IQ will be autogenerated if it is not there yet.
		Returns a deferred which will fire with the matching IQ response as it's sole argument. """
		def checkDeferred():
			if not d.called:
				d.errback()
				del self.deferredIqs[(jid, ID)]

		jid = el.getAttribute("to")
		ID = el.getAttribute("id")
		if not ID:
			ID = self.pytrans.makeMessageID()
			el.attributes["id"] = ID
		self.pytrans.send(el)
		d = Deferred()
		self.deferredIqs[(jid, ID)] = d
		reactor.callLater(timeout, checkDeferred)
		return d

	def addIdentity(self, category, ctype, name, jid):
		""" Adds an identity to this JID's discovery profile. If jid == "USER" then AIM users will get this identity. """
		debug.log("ServerDiscovery: Adding identity \"%s\" \"%s\" \"%s\" \"%s\"" % (category, ctype, name, jid))
		if not self.identities.has_key(jid):
			self.identities[jid] = []
		self.identities[jid].append((category, ctype, name))
	
	def addFeature(self, var, handler, jid):
		""" Adds a feature to this JID's discovery profile. If jid == "USER" then AIM users will get this feature. """
		debug.log("ServerDiscovery: Adding feature support \"%s\" \"%s\" \"%s\"" % (var, handler, jid))
		if not self.features.has_key(jid):
			self.features[jid] = []
		self.features[jid].append((var, handler))

	def addNode(self, node, handler, name, jid, rootnode):
		""" Adds a node to this JID's discovery profile. If jid == "USER" then AIM users will get this node. """
		debug.log("ServerDiscovery: Adding node item \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" % (node, handler, name, jid, rootnode))
		if not self.nodes.has_key(jid):
			self.nodes[jid] = {}
		self.nodes[jid][node] = (handler, name, rootnode)

	def onIq(self, el):
		""" Decides what to do with an IQ """
		debug.log("ServerDiscovery: onIq element \"%s\"" % (el.toXml()))
		fro = el.getAttribute("from")
		to = el.getAttribute("to")
		ID = el.getAttribute("id")
		iqType = el.getAttribute("type")
		ulang = utils.getLang(el)
		try: # StringPrep
			froj = jid.JID(fro)
			to = jid.JID(to).full()
		except Exception, e:
			debug.log("ServerDiscovery: Dropping IQ because of stringprep error - \"%s\" \"%s\" %s" % (fro, to, str(e)))

		# Check if it's a response to a sent IQ
		if self.deferredIqs.has_key((fro, ID)) and (iqType == "error" or iqType == "result"):
			debug.log("ServerDiscovery: Iq received \"%s\" \"%s\". Doing callback." % (fro, ID))
			self.deferredIqs[(fro, ID)].callback(el)
			del self.deferredIqs[(fro, ID)]
			return

		if not (iqType == "get" or iqType == "set"): return # Not interested

		debug.log("ServerDiscovery: Iq received \"%s\" \"%s\". Looking for handler" % (fro, ID))

		for query in el.elements():
			xmlns = query.defaultUri
			node = query.getAttribute("node")

			if xmlns.startswith(globals.DISCO) and node:
				if self.nodes.has_key(to) and self.nodes[to].has_key(node) and self.nodes[to][node][0] != None:
					self.nodes[to][node][0](el)
					return
				else:
					# If the node we're browsing wasn't found, fall through and display the root disco
					self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
					return
			elif xmlns == globals.DISCO_INFO:
				self.sendDiscoInfoResponse(to=fro, ID=ID, ulang=ulang, jid=to)
				return
			elif xmlns == globals.DISCO_ITEMS:
				self.sendDiscoItemsResponse(to=fro, ID=ID, ulang=ulang, jid=to)
				return

			if to.find('@') > 0:
				searchjid = "USER"
			else:
				searchjid = to

			for (feature, handler) in self.features.get(searchjid, []):
					if feature == xmlns and handler:
						debug.log("ServerDiscovery: Handler found \"%s\" \"%s\"" % (feature, handler))
						handler(el)
						return

			# Still hasn't been handled
			debug.log("Discovery: Unknown Iq request \"%s\" \"%s\" \"%s\"" % (fro, ID, xmlns))
			self.sendIqError(to=fro, fro=to, ID=ID, xmlns=xmlns, etype="cancel", condition="feature-not-implemented")

	def sendDiscoInfoResponse(self, to, ID, ulang, jid):
		""" Send a service discovery disco#info stanza to the given 'to'. 'jid' is the JID that was queried. """
		debug.log("ServerDiscovery: Replying to disco#info request from \"%s\" \"%s\"" % (to, ID))
		iq = Element((None, "iq"))
		iq.attributes["type"] = "result"
		iq.attributes["from"] = jid
		iq.attributes["to"] = to
		if ID:
			iq.attributes["id"] = ID
		query = iq.addElement("query")
		query.attributes["xmlns"] = globals.DISCO_INFO
		
		searchjid = jid
		if jid.find('@') > 0: searchjid = "USER"

		# Add any identities
		for (category, ctype, name) in self.identities.get(searchjid, []):
			debug.log("Found identity %s %s %s" % (category, ctype, name))
			identity = query.addElement("identity")
			identity.attributes["category"] = category
			identity.attributes["type"] = ctype
			identity.attributes["name"] = name
		
		# Add any supported features
		for (var, handler) in self.features.get(searchjid, []):
			debug.log("Found feature %s" % (var))
			feature = query.addElement("feature")
			feature.attributes["var"] = var

		self.pytrans.send(iq)

	def sendDiscoItemsResponse(self, to, ID, ulang, jid):
		""" Send a service discovery disco#items stanza to the given 'to'. 'jid' is the JID that was queried. """
		debug.log("ServerDiscovery: Replying to disco#items request from \"%s\" \"%s\"" % (to, ID))
		iq = Element((None, "iq"))
		iq.attributes["type"] = "result"
		iq.attributes["from"] = jid
		iq.attributes["to"] = to
		if ID:
			iq.attributes["id"] = ID
		query = iq.addElement("query")
		query.attributes["xmlns"] = globals.DISCO_ITEMS

		searchjid = jid
		if jid.find('@') > 0: searchjid = "USER"

		for node in self.nodes.get(searchjid, []):
			handler, name, rootnode = self.nodes[jid][node]
			if rootnode:
				debug.log("Found node %s" % (node))
				name = lang.get(name, ulang)
				item = query.addElement("item")
				item.attributes["jid"] = jid
				item.attributes["node"] = node
				item.attributes["name"] = name
		
		self.pytrans.send(iq)
	
	def sendIqError(self, to, fro, ID, xmlns, etype, condition):
		""" Sends an IQ error response. See the XMPP RFC for details on the fields. """
		debug.log("Sending IQ Error: %s %s %s %s %s" % (to,fro,xmlns,etype,condition))
		el = Element((None, "iq"))
		el.attributes["to"] = to
		el.attributes["from"] = fro
		if ID:
			el.attributes["id"] = ID
		el.attributes["type"] = "error"
		error = el.addElement("error")
		error.attributes["type"] = etype
		error.attributes["code"] = str(utils.errorCodeMap[condition])
		cond = error.addElement(condition)
		self.pytrans.send(el)
